CONTINUE: 3,
};
-/** @enum {number} */
-const DragState = {
- INIT: 0,
- DRAGGING: 1,
- CANCELLED: 2,
-};
-
const DRAG_CURSOR_MAP = {
0: Meta.Cursor.NO_DROP,
1: Meta.Cursor.COPY,
class _Draggable extends Signals.EventEmitter {
constructor(actor, params) {
super();
-
params = Params.parse(params, {
manualMode: false,
timeoutThreshold: 0,
});
this.actor = actor;
- this._dragState = DragState.INIT;
-
- if (!params.manualMode) {
- this.actor.connect('button-press-event',
- this._onButtonPress.bind(this));
- this.actor.connect('touch-event',
- this._onTouchEvent.bind(this));
- }
- this.actor.connect('destroy', () => {
- this._actorDestroyed = true;
-
- if (this._dragState === DragState.DRAGGING && this._dragCancellable)
- this._cancelDrag(global.get_current_time());
+ this._dndGesture = new St.DndStartGesture({
+ manual_mode: params.manualMode,
+ timeout_threshold: params.timeoutThreshold,
});
- this._onEventId = null;
- this._touchSequence = null;
+ this._dndGesture.connect('recognize', () => this._gestureRecognized());
+
+ this.actor.add_action(this._dndGesture);
this._restoreOnSuccess = params.restoreOnSuccess;
this._dragActorMaxSize = params.dragActorMaxSize;
this._dragCancellable = true;
}
- /**
- * addClickAction:
- *
- * @param {Clutter.ClickAction} action - click action to add to draggable actor
- *
- * Add @action to the draggable's actor, and set it up so that it does not
- * impede drag operations.
- */
- addClickAction(action) {
- action.connect('clicked', () => (this._actionClicked = true));
- action.connect('long-press', (a, actor, state) => {
- if (state !== Clutter.LongPressState.CANCEL)
- return true;
-
- const event = Clutter.get_current_event();
- this._dragTouchSequence = event.get_event_sequence();
-
- if (this._longPressLater)
- return true;
-
- // A click cancels a long-press before any click handler is
- // run - make sure to not start a drag in that case
- const laters = global.compositor.get_laters();
- this._longPressLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
- delete this._longPressLater;
- if (this._actionClicked) {
- delete this._actionClicked;
- return GLib.SOURCE_REMOVE;
- }
- action.release();
- this.startDrag(
- ...action.get_coords(),
- event.get_time(),
- this._dragTouchSequence,
- event.get_device());
-
- return GLib.SOURCE_REMOVE;
- });
- return true;
- });
-
- this.actor.add_action(action);
- }
-
- _onButtonPress(actor, event) {
- if (event.get_button() !== 1)
- return Clutter.EVENT_PROPAGATE;
-
- this._grabActor(event.get_device());
-
- let [stageX, stageY] = event.get_coords();
- this._dragStartX = stageX;
- this._dragStartY = stageY;
- this._dragStartTime = event.get_time();
- this._dragThresholdIgnored = false;
-
- return Clutter.EVENT_PROPAGATE;
- }
-
- _onTouchEvent(actor, event) {
- // We only handle touch events here on wayland. On X11
- // we do get emulated pointer events, which already works
- // for single-touch cases. Besides, the X11 passive touch grab
- // set up by Mutter will make us see first the touch events
- // and later the pointer events, so it will look like two
- // unrelated series of events, we want to avoid double handling
- // in these cases.
- if (!Meta.is_wayland_compositor())
- return Clutter.EVENT_PROPAGATE;
-
- if (event.type() !== Clutter.EventType.TOUCH_BEGIN ||
- !global.display.is_pointer_emulating_sequence(event.get_event_sequence()))
- return Clutter.EVENT_PROPAGATE;
-
- this._grabActor(event.get_device(), event.get_event_sequence());
- this._dragStartTime = event.get_time();
- this._dragThresholdIgnored = false;
-
- let [stageX, stageY] = event.get_coords();
- this._dragStartX = stageX;
- this._dragStartY = stageY;
-
- return Clutter.EVENT_PROPAGATE;
- }
-
- _grabDevice(actor, pointer, touchSequence) {
- this._grab = global.stage.grab(actor);
- this._grabbedDevice = pointer;
- this._touchSequence = touchSequence;
- }
-
- _ungrabDevice() {
- if (this._grab) {
- this._grab.dismiss();
- this._grab = null;
- }
- this._touchSequence = null;
- this._grabbedDevice = null;
- }
-
- _grabActor(device, touchSequence) {
- this._grabDevice(this.actor, device, touchSequence);
- this._onEventId = this.actor.connect('event',
- this._onEvent.bind(this));
- }
-
- _ungrabActor() {
- if (!this._onEventId)
- return;
-
- this._ungrabDevice();
- this.actor.disconnect(this._onEventId);
- this._onEventId = null;
- }
-
_grabEvents(device, touchSequence) {
- if (!this._eventsGrab) {
- let grab = Main.pushModal(_getEventHandlerActor());
- if ((grab.get_seat_state() & Clutter.GrabState.POINTER) !== 0) {
- this._grabDevice(_getEventHandlerActor(), device, touchSequence);
- this._eventsGrab = grab;
- } else {
- Main.popModal(grab);
- }
- }
- }
-
- _ungrabEvents() {
- if (this._eventsGrab) {
- this._ungrabDevice();
- Main.popModal(this._eventsGrab);
- this._eventsGrab = null;
- }
- }
-
- _eventIsRelease(event) {
- if (event.type() === Clutter.EventType.BUTTON_RELEASE) {
- let buttonMask = Clutter.ModifierType.BUTTON1_MASK |
- Clutter.ModifierType.BUTTON2_MASK |
- Clutter.ModifierType.BUTTON3_MASK;
- /* We only obey the last button release from the device,
- * other buttons may get pressed/released during the DnD op.
- */
- return (event.get_state() & buttonMask) === 0;
- } else if (event.type() === Clutter.EventType.TOUCH_END) {
- /* For touch, we only obey the pointer emulating sequence */
- return global.display.is_pointer_emulating_sequence(event.get_event_sequence());
+ const grab = Main.pushModal(_getEventHandlerActor());
+ if ((grab.get_seat_state() & Clutter.GrabState.POINTER) === 0) {
+ Main.popModal(grab);
+ return;
}
- return false;
+ this._grab = grab;
+ this._grabbedDevice = device;
+ this._grabbedEventSequence = touchSequence;
}
- _onEvent(actor, event) {
- let device = event.get_device();
-
- if (this._grabbedDevice &&
- device !== this._grabbedDevice &&
- device.get_device_type() !== Clutter.InputDeviceType.KEYBOARD_DEVICE)
- return Clutter.EVENT_PROPAGATE;
-
- // We intercept BUTTON_RELEASE event to know that the button was released in case we
- // didn't start the drag, to drop the draggable in case the drag was in progress, and
- // to complete the drag and ensure that whatever happens to be under the pointer does
- // not get triggered if the drag was cancelled with Esc.
- if (this._eventIsRelease(event)) {
- if (this._dragState === DragState.DRAGGING) {
- return this._dragActorDropped(event);
- } else if ((this._dragActor != null || this._dragState === DragState.CANCELLED) &&
- !this._animationInProgress) {
- // Drag must have been cancelled with Esc.
- this._dragComplete();
- return Clutter.EVENT_STOP;
- } else {
- // Drag has never started.
- this._ungrabActor();
- return Clutter.EVENT_PROPAGATE;
- }
- // We intercept MOTION event to figure out if the drag has started and to draw
- // this._dragActor under the pointer when dragging is in progress
- } else if (event.type() === Clutter.EventType.MOTION ||
- (event.type() === Clutter.EventType.TOUCH_UPDATE &&
- global.display.is_pointer_emulating_sequence(event.get_event_sequence()))) {
- if (this._dragActor && this._dragState === DragState.DRAGGING)
- return this._updateDragPosition(event);
- else if (this._dragActor == null && this._dragState !== DragState.CANCELLED)
- return this._maybeStartDrag(event);
-
- // We intercept KEY_PRESS event so that we can process Esc key press to cancel
- // dragging and ignore all other key presses.
- } else if (event.type() === Clutter.EventType.KEY_PRESS && this._dragState === DragState.DRAGGING) {
- let symbol = event.get_key_symbol();
- if (symbol === Clutter.KEY_Escape) {
- this._cancelDrag(event.get_time());
- return Clutter.EVENT_STOP;
- }
- }
-
- return Clutter.EVENT_PROPAGATE;
+ _ungrabEvents() {
+ Main.popModal(this._grab);
+ this._grab = null;
+ this._grabbedDevice = null;
+ this._grabbedEventSequence = null;
}
/**
* PopupMenu.ignoreRelease())
*/
fakeRelease() {
- this._ungrabActor();
+ this._dndGesture.set_state(Clutter.GestureState.CANCELLED);
}
- /**
- * Directly initiate a drag and drop operation from the given actor.
- * This function is useful to call if you've specified manualMode
- * for the draggable.
- *
- * @param {number} stageX - X coordinate of event
- * @param {number} stageY - Y coordinate of event
- * @param {number} time - Event timestamp
- * @param {Clutter.EventSequence=} sequence - Event sequence
- * @param {Clutter.InputDevice=} device - device that originated the event
- */
- startDrag(stageX, stageY, time, sequence, device) {
- if (currentDraggable)
- return;
-
- if (device === undefined) {
- let event = Clutter.get_current_event();
+ _gestureRecognized() {
+ const pointBeginEvent = this._dndGesture.get_point_begin_event();
+ [this._dragStartX, this._dragStartY] = pointBeginEvent.get_coords();
- if (event)
- device = event.get_device();
-
- if (device === undefined) {
- const backend = this.actor.get_context().get_backend();
- const seat = backend.get_default_seat();
- device = seat.get_pointer();
- }
- }
+ const triggeringEvent = this._dndGesture.get_drag_triggering_event();
+ const [stageX, stageY] = triggeringEvent.get_coords();
+ const device = triggeringEvent.get_device();
+ const sequence = triggeringEvent.get_event_sequence();
+ const time = triggeringEvent.get_time();
currentDraggable = this;
- this._dragState = DragState.DRAGGING;
- // Special-case St.Button: the pointer grab messes with the internal
- // state, so force a reset to a reasonable state here
- if (this.actor instanceof St.Button) {
- this.actor.fake_release();
- this.actor.hover = false;
- }
+ this._grabEvents(device, sequence);
+ if (!this._grab)
+ return;
this.emit('drag-begin', time);
- if (this._onEventId)
- this._ungrabActor();
-
- this._grabEvents(device, sequence);
global.display.set_cursor(Meta.Cursor.NO_DROP);
- this._dragX = this._dragStartX = stageX;
- this._dragY = this._dragStartY = stageY;
+ this._dragX = stageX;
+ this._dragY = stageY;
let scaledWidth, scaledHeight;
// around the pointer
let [sourceX, sourceY] = this._dragActorSource.get_transformed_position();
let x, y;
- if (stageX > sourceX && stageX <= sourceX + this._dragActor.width &&
- stageY > sourceY && stageY <= sourceY + this._dragActor.height) {
+ if (this._dragStartX > sourceX && this._dragStartX <= sourceX + this._dragActor.width &&
+ this._dragStartY > sourceY && this._dragStartY <= sourceY + this._dragActor.height) {
x = sourceX;
y = sourceY;
} else {
- x = stageX - this._dragActor.width / 2;
- y = stageY - this._dragActor.height / 2;
+ x = this._dragStartX - this._dragActor.width / 2;
+ y = this._dragStartY - this._dragActor.height / 2;
}
this._dragActor.set_position(x, y);
this._dragActorSource = undefined;
this._dragOrigParent = this.actor.get_parent();
+
this._dragActorHadFixedPos = this._dragActor.fixed_position_set;
this._dragOrigX = this._dragActor.allocation.x1;
this._dragOrigY = this._dragActor.allocation.y1;
this._finishAnimation();
this._dragActor = null;
- if (this._dragState === DragState.DRAGGING)
- this._dragState = DragState.CANCELLED;
+
+ if (this._dragCancellable)
+ this._cancelDrag(global.get_current_time());
});
this._dragOrigOpacity = this._dragActor.opacity;
if (this._dragActorOpacity !== undefined)
this._dragY + this._dragOffsetY);
}
- _maybeStartDrag(event) {
- let [stageX, stageY] = event.get_coords();
-
- if (this._dragThresholdIgnored)
- return Clutter.EVENT_PROPAGATE;
-
- // See if the user has moved the mouse enough to trigger a drag
- let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
- let threshold = St.Settings.get().drag_threshold * scaleFactor;
- if (!currentDraggable &&
- (Math.abs(stageX - this._dragStartX) > threshold ||
- Math.abs(stageY - this._dragStartY) > threshold)) {
- const deviceType = event.get_source_device().get_device_type();
- const isPointerOrTouchpad =
- deviceType === Clutter.InputDeviceType.POINTER_DEVICE ||
- deviceType === Clutter.InputDeviceType.TOUCHPAD_DEVICE;
- const ellapsedTime = event.get_time() - this._dragStartTime;
-
- // Pointer devices (e.g. mouse) start the drag immediately
- if (isPointerOrTouchpad || ellapsedTime > this._dragTimeoutThreshold) {
- this.startDrag(stageX, stageY, event.get_time(), this._touchSequence, event.get_device());
- this._updateDragPosition(event);
- } else {
- this._dragThresholdIgnored = true;
- this._ungrabActor();
- return Clutter.EVENT_PROPAGATE;
- }
- }
-
- return Clutter.EVENT_STOP;
- }
-
_pickTargetActor() {
return this._dragActor.get_stage().get_actor_at_pos(
Clutter.PickMode.ALL, this._dragX, this._dragY);
switch (dropFunc(dropEvent)) {
case DragDropResult.FAILURE:
case DragDropResult.SUCCESS:
- return true;
+ return;
+
case DragDropResult.CONTINUE:
continue;
}
if (this._dragActor && this._dragActor.get_parent() === Main.uiGroup) {
if (this._restoreOnSuccess) {
this._restoreDragActor(event.get_time());
- return true;
+ return;
} else {
this._dragActor.destroy();
}
}
- this._dragState = DragState.INIT;
global.display.set_cursor(Meta.Cursor.DEFAULT);
this.emit('drag-end', event.get_time(), true);
this._dragComplete();
- return true;
+ return;
}
}
target = target.get_parent();
}
+ // If no target has been found, cancel the drag
this._cancelDrag(event.get_time());
-
- return true;
}
_getRestoreLocation() {
return [x, y, scale];
}
- _cancelDrag(eventTime) {
- this.emit('drag-cancelled', eventTime);
- let wasCancelled = this._dragState === DragState.CANCELLED;
- this._dragState = DragState.CANCELLED;
-
- if (this._actorDestroyed || wasCancelled) {
- global.display.set_cursor(Meta.Cursor.DEFAULT);
- this._dragComplete();
- this.emit('drag-end', eventTime, false);
- if (!this._dragOrigParent && this._dragActor)
- this._dragActor.destroy();
-
- return;
- }
-
- let [snapBackX, snapBackY, snapBackScale] = this._getRestoreLocation();
-
- this._animateDragEnd(eventTime, {
- x: snapBackX,
- y: snapBackY,
- scale_x: snapBackScale,
- scale_y: snapBackScale,
- duration: SNAP_BACK_ANIMATION_TIME,
- });
- }
-
_restoreDragActor(eventTime) {
- this._dragState = DragState.INIT;
let [restoreX, restoreY, restoreScale] = this._getRestoreLocation();
// fade the actor back in at its original location
...params,
opacity: this._dragOrigOpacity,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
- onComplete: () => {
- this._onAnimationComplete(this._dragActor, eventTime);
+ onStopped: () => {
+ this._onAnimationComplete(eventTime);
},
});
}
global.display.set_cursor(Meta.Cursor.DEFAULT);
}
- _onAnimationComplete(dragActor, eventTime) {
+ _onAnimationComplete(eventTime) {
if (this._dragOrigParent) {
Main.uiGroup.remove_child(this._dragActor);
this._dragOrigParent.add_child(this._dragActor);
- dragActor.set_scale(this._dragOrigScale, this._dragOrigScale);
+ this._dragActor.set_scale(this._dragOrigScale, this._dragOrigScale);
if (this._dragActorHadFixedPos)
- dragActor.set_position(this._dragOrigX, this._dragOrigY);
+ this._dragActor.set_position(this._dragOrigX, this._dragOrigY);
else
- dragActor.fixed_position_set = false;
+ this._dragActor.fixed_position_set = false;
if (this._dragActorHadNatWidth)
this._dragActor.set_width(-1);
if (this._dragActorHadNatHeight)
this._dragActor.set_height(-1);
} else {
- dragActor.destroy();
+ this._dragActor?.destroy();
}
this.emit('drag-end', eventTime, false);
}
_dragComplete() {
- if (!this._actorDestroyed && this._dragActor)
+ if (this._dragActor)
Shell.util_set_hidden_from_pick(this._dragActor, false);
- this._ungrabEvents();
+ if (this._grab)
+ this._ungrabEvents();
if (this._updateHoverId) {
GLib.source_remove(this._updateHoverId);
this._dragActorSource.disconnect(this._dragActorSourceDestroyId);
this._dragActorSource = null;
}
+ }
+
+ _onEvent(actor, event) {
+ if (event.type() === Clutter.EventType.KEY_PRESS) {
+ if (event.get_key_symbol() === Clutter.KEY_Escape)
+ this._cancelDrag(event.get_time());
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ if (event.get_device() !== this._grabbedDevice)
+ return Clutter.EVENT_PROPAGATE;
+
+ const sequence = event.get_event_sequence();
+ if (this._grabbedEventSequence &&
+ sequence?.get_slot() !== this._grabbedEventSequence.get_slot())
+ return Clutter.EVENT_PROPAGATE;
+
+ if (event.type() === Clutter.EventType.BUTTON_RELEASE ||
+ event.type() === Clutter.EventType.TOUCH_END)
+ this._dragActorDropped(event);
+
+ if (event.type() === Clutter.EventType.MOTION ||
+ event.type() === Clutter.EventType.TOUCH_UPDATE)
+ this._updateDragPosition(event);
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _cancelDrag(eventTime) {
+ this.emit('drag-cancelled', eventTime);
+
+ if (!this._dragActor) {
+ global.display.set_cursor(Meta.Cursor.DEFAULT);
+ this._dragComplete();
+ this.emit('drag-end', eventTime, false);
+ if (!this._dragOrigParent && this._dragActor)
+ this._dragActor.destroy();
+
+ return;
+ }
+
+ let [snapBackX, snapBackY, snapBackScale] = this._getRestoreLocation();
+
+ this._animateDragEnd(eventTime, {
+ x: snapBackX,
+ y: snapBackY,
+ scale_x: snapBackScale,
+ scale_y: snapBackScale,
+ duration: SNAP_BACK_ANIMATION_TIME,
+ });
+ }
- this._dragState = DragState.INIT;
- currentDraggable = null;
+ get startGesture() {
+ return this._dndGesture;
}
}